/**
 * Copyright (C) 2008 Christopher Allen Ogden
 *
 *     This file is part of Odysi.
 *
 *  Odysi is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Odysi is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Odysi.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <fstream>
#include <stdexcept>
#include <stdint.h>
#include "renderSystem.h"
#include "config.h"
#include "util.h"
#include "pixel.h"

using std::string;
using std::runtime_error;
using std::ifstream;

void RenderSystem::quad(GLuint texture,
                        GLint x1,
                        GLint y1,
                        GLint x2,
                        GLint y2,
                        GLfloat u1,
                        GLfloat v1,
                        GLfloat u2,
                        GLfloat v2)
{
    // Create the quad.
    Quad *q = new Quad();
    q->texture = texture;

    // Set the vertices.
    q->vertex1[0] = x1;
    q->vertex1[1] = y1;
    q->vertex2[0] = x2;
    q->vertex2[1] = y1;
    q->vertex3[0] = x2;
    q->vertex3[1] = y2;
    q->vertex4[0] = x1;
    q->vertex4[1] = y2;

    // Set the texture coordinates.
    q->uv1[0] = u1;
    q->uv1[1] = v2;
    q->uv2[0] = u2;
    q->uv2[1] = v2;
    q->uv3[0] = u2;
    q->uv3[1] = v1;
    q->uv4[0] = u1;
    q->uv4[1] = v1;

    // Create the command.
    Command c;
    c.command = Command::QUAD;
    c.data = q;
    _commands.push_back(c);
}

void RenderSystem::solidQuad(GLint x1, GLint y1, GLint x2, GLint y2)
{
    // Create the quad.
    SolidQuad *q = new SolidQuad();

    // Set the vertices.
    q->vertex1[0] = x1;
    q->vertex1[1] = y1;
    q->vertex2[0] = x2;
    q->vertex2[1] = y1;
    q->vertex3[0] = x2;
    q->vertex3[1] = y2;
    q->vertex4[0] = x1;
    q->vertex4[1] = y2;

    // Create the command.
    Command c;
    c.command = Command::SOLIDQUAD;
    c.data = q;
    _commands.push_back(c);
}

void RenderSystem::line(GLint x1, GLint y1, GLint x2, GLint y2)
{
    Line *l = new Line();

    // Set the vertices.
    l->vertex1[0] = x1;
    l->vertex1[1] = y1;
    l->vertex2[0] = x2;
    l->vertex2[1] = y2;

    // Create the command.
    Command c;
    c.command = Command::LINE;
    c.data = l;
    _commands.push_back(c);
}

void RenderSystem::emptyQuad(GLint x1, GLint y1, GLint x2, GLint y2)
{
    line(x1, y1, x2, y1);
    line(x2, y1, x2, y2);
    line(x2, y2, x1, y2);
    line(x1, y2, x1, y1);
}

void RenderSystem::image(unsigned int image, int x, int y)
{
    Image img = _images[image];
    quad(img.texture, x, y, x + img.width, y + img.height, img.u1, img.v1, img.u2, img.v2);
}

void RenderSystem::color(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha)
{
    // Create the color data.
    GLfloat *v = new GLfloat[4];
    v[0] = red;
    v[1] = green;
    v[2] = blue;
    v[3] = alpha;

    // Create the command.
    Command c;
    c.command = Command::COLOR;
    c.data = v;
    _commands.push_back(c);
}

void RenderSystem::render(void)
{
    // Begin OpenGL rendering.
    glBegin(GL_QUADS);

    // Loop through all the commands.
    foreach(Command c, _commands)
    {
        // Decide which command to process.
        switch(c.command)
        {
            case Command::QUAD:
            {
                // Draw a quad.
                Quad *q = reinterpret_cast<Quad *>(c.data);
                if(_lastTexture != q->texture)
                {
                    glEnd();
                    glBindTexture(GL_TEXTURE_2D, q->texture);
                    _lastTexture = q->texture;
                    glBegin(GL_QUADS);
                }
                glTexCoord2fv(q->uv1); glVertex2iv(q->vertex1);
                glTexCoord2fv(q->uv2); glVertex2iv(q->vertex2);
                glTexCoord2fv(q->uv3); glVertex2iv(q->vertex3);
                glTexCoord2fv(q->uv4); glVertex2iv(q->vertex4);
                delete q;
                break;
            }
            case Command::COLOR:
            {
                // Change the color.
                GLfloat *v = reinterpret_cast<GLfloat *>(c.data);
                glColor4fv(v);
                delete []v;
                break;
            }
            case Command::SOLIDQUAD:
            {
                // Draw a solid quad.
                SolidQuad *q = reinterpret_cast<SolidQuad *>(c.data);
                if(_lastTexture != 0)
                {
                    glEnd();
                    glBindTexture(GL_TEXTURE_2D, 0);
                    _lastTexture = 0;
                    glBegin(GL_QUADS);
                }
                glVertex2iv(q->vertex1);
                glVertex2iv(q->vertex2);
                glVertex2iv(q->vertex3);
                glVertex2iv(q->vertex4);
                delete q;
                break;
            }
            case Command::LINE:
            {
                // Draw a line.
                Line *l = reinterpret_cast<Line *>(c.data);
                glEnd();
                glBindTexture(GL_TEXTURE_2D, 0);
                _lastTexture = 0;
                glBegin(GL_LINES);
                glVertex2iv(l->vertex1);
                glVertex2iv(l->vertex2);
                glEnd();
                glBegin(GL_QUADS);
                delete l;
                break;

            }
        }
    }
    
    // End OpenGL rendering.
    glEnd();

    // Clear the command list.
    _commands.clear();
}

RenderSystem::~RenderSystem()
{
    // Loop through all the commands and delete their data.
    foreach(Command c, _commands)
    {
        switch(c.command)
        {
            case Command::QUAD:
                delete reinterpret_cast<Quad *>(c.data);
                break;
            case Command::COLOR:
                delete []reinterpret_cast<GLfloat *>(c.data);
                break;
            case Command::SOLIDQUAD:
                delete reinterpret_cast<SolidQuad *>(c.data);
                break;
            case Command::LINE:
                delete reinterpret_cast<Line *>(c.data);
                break;
        }
    }
    _commands.clear();
}

unsigned int RenderSystem::load(const std::string &imageFile)
{
    ifstream file(imageFile.c_str(), std::ios_base::binary);
    if(!file)
        throw runtime_error("Could not open the image file '" + imageFile + "' for loading.");

    uint8_t idFieldLength; // Length of the identification field.
    if(!file.read(reinterpret_cast<char *>(&idFieldLength), 1))
        throw runtime_error("Could not get the identification field for the image file '" + imageFile + "' during loading.");

    uint8_t colorMapType; // Color map type.  This should be zero for true-color images.
    if(!file.read(reinterpret_cast<char *>(&colorMapType), 1))
        throw runtime_error("Could not get the color map type from the image file '" + imageFile + "' during loading.");
    if(colorMapType)
        throw runtime_error("Unsupported color map type in image file '" + imageFile + "' during loading.  This might mean that the image is not true-color, and this application can only load true-color images.");

    uint8_t imageTypeCode; // Image type.  This should be 2.
    if(!file.read(reinterpret_cast<char *>(&imageTypeCode), 1))
        throw runtime_error("Could not get the image type code from the image file '" + imageFile = "' during loading.");
    if(imageTypeCode != 2)
        throw runtime_error("Unsupported image type code in the image file '" + imageFile + "' found during loading.");

    uint8_t colorMapSpec[5]; // Color map information.  This can be ignored for true-color images.
    if(!file.read(reinterpret_cast<char *>(colorMapSpec), 5))
        throw runtime_error("Could not read the color map information from the image file '" + imageFile + "' during loading.");

    short xOrig; // X origin of the image.
    if(!file.read(reinterpret_cast<char *>(&xOrig), 2))
        throw runtime_error("Could not load the X origin of the image from the image file '" + imageFile + "' during loading.");

    short yOrig; // Y origin of the image.
    if(!file.read(reinterpret_cast<char *>(&yOrig), 2))
        throw runtime_error("Could not load the Y origin of the image from the image file '" + imageFile + "' during loading.");

    short width; // Width of the image.
    if(!file.read(reinterpret_cast<char *>(&width), 2))
        throw runtime_error("Could not load the width of the image from the image file '" + imageFile + "' during loading.");
    if(!powerOfTwo(width)) // The width must be a power of two.
        throw runtime_error("The width of the image in the image file '" + imageFile + "' must be a power of 2.");

    short height; // Height of the image.
    if(!file.read(reinterpret_cast<char *>(&height), 2))
        throw runtime_error("Could not load the height of the image from the image file '" + imageFile + "' during loading.");
    if(!powerOfTwo(height)) // The height must be a power of two.
        throw runtime_error("The height of the image in the image file '" + imageFile + "' must be a power of 2.");

    uint8_t numBits; // Number of bits per pixel.
    if(!file.read(reinterpret_cast<char *>(&numBits), 1))
        throw runtime_error("Could not read the number of bits per pixel from the image file '" + imageFile + "' during loading.");
    if(numBits != 24 && numBits != 32)
        throw runtime_error("The bit depth in the image file '" + imageFile + "' must be either 24 or 32.");

    uint8_t imageDescriptor; // This byte contains flags.
    if(!file.read(reinterpret_cast<char *>(&imageDescriptor), 1))
        throw runtime_error("Could not load the image descriptor from the image file '" + imageFile + "' during loading.");

    char imageIdentification[256]; // Optional image identification.
    memset(imageIdentification, 0, 256);
    if(!file.read(imageIdentification, idFieldLength))
        throw runtime_error("Could not load the image identification from the image file '" + imageFile + "' during loading.");

    PixelData data;

    for(int i = 0; i < width * height; i++)
    {
        Pixel p;
        p.alpha = 255;
        if(!file.read(reinterpret_cast<char *>(&p.blue), 1) || !file.read(reinterpret_cast<char *>(&p.green), 1) || !file.read(reinterpret_cast<char *>(&p.red), 1))
            throw runtime_error("There was an error reading the pixel data in the image file '" + imageFile + "' during loading.");

        if(numBits == 32)
        {
            if(!file.read(reinterpret_cast<char *>(&p.alpha), 1))
                throw runtime_error("There was an error reading the alpha channel in the image file '" + imageFile + "' during loading.");
        }
        data.push_back(p);
    }

    GLuint id;

    glGenTextures(1, &id);
    glBindTexture(GL_TEXTURE_2D, id);

    uint8_t *pixels = new uint8_t[width * height * 4];

    for(unsigned int y = 0; y < height; y++)
    {
        for(unsigned int x = 0; x < width; x++)
        {
            unsigned int index = (y * width + x) * 4;
            int index2 = y * width + x;
            pixels[index] = data[index2].red;
            pixels[index + 1] = data[index2].green;
            pixels[index + 2] = data[index2].blue;
            pixels[index + 3] = data[index2].alpha;
        }
    }

    glTexImage2D(GL_TEXTURE_2D, 0, 4, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);

    delete [] pixels;

    Image image;
    image.texture = id;
    image.u1 = 0.0f;
    image.u2 = 1.0f;
    image.v1 = 0.0f;
    image.v2 = 1.0f;
    image.width = width;
    image.height = height;
    image.fileName = imageFile;

    _images.push_back(image);

    return _images.size() - 1;
}
